/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.client.editor.model;

import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Shape;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;

import java.util.LinkedList;
import javax.swing.JEditorPane;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.View;
import javax.swing.text.html.CSS;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTML.Tag;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.HTMLWriter;

/**
 *
 * @author Walter Lütgenau
 */
public class EditorModel {

    DocumentContext context;
    static HashSet<String> containers = new HashSet<String>(Arrays.asList(
            new String[] { HTML.Tag.BODY.toString(), HTML.Tag.DIV.toString(), HTML.Tag.TD.toString(), HTML.Tag.LI.toString() }));

    static final HashMap<String, HTML.Tag[]> structureParents = new HashMap<String, HTML.Tag[]>();
    static {
        structureParents.put("td", new HTML.Tag[] {HTML.Tag.TR});
        structureParents.put("tr", new HTML.Tag[] {HTML.Tag.TABLE});
        structureParents.put("th", new HTML.Tag[] {HTML.Tag.TABLE});
        structureParents.put("li", new HTML.Tag[] {HTML.Tag.OL, HTML.Tag.UL});
        structureParents.put("", new HTML.Tag[] {HTML.Tag.P});
    }
    
    public static final String HTML_CR = "CR";

    /** Creates a new instance of EditorModel */
    public EditorModel(DocumentContext context) {
        this.context = context;
    }

    public DocumentContext getContext() {
        return context;
    }
    

    public boolean isElementTag(Element element, HTML.Tag tag) {
        return element.getName().equalsIgnoreCase(tag.toString());
    }

    public boolean isElementTag(int position, HTML.Tag tag) {
        return isElementTag(context.getDocument().getParagraphElement(position), tag);
    }

    public boolean isParentElementTag(int position, HTML.Tag tag) {
        Element element = context.getDocument().getParagraphElement(position);
        return isElementTag(element.getParentElement(), tag);
    }

    public boolean isParagraph(Element e) {
        return e.getName().equals(HTML.Tag.IMPLIED.toString()) || e.getName().equals(HTML.Tag.P.toString());
    }

    public boolean isWithinRange(Element e, Range range) {
        return (e.getStartOffset() <= range.getStartOffset() && e.getEndOffset() > range.getStartOffset())
                || (e.getStartOffset() <= range.getEndOffset() && e.getEndOffset() > range.getEndOffset())
                || (range.getStartOffset() < e.getStartOffset() && range.getEndOffset() > e.getEndOffset());
    }

    public Element getRealParagraph(int pos) {
        Element e = getDocument().getParagraphElement(pos);
        if (e.getName().equals(HTML.Tag.IMPLIED.toString()))
            e = e.getParentElement();
        return e;
    }

    Element[] getPathArray(Element e) {
        LinkedList<Element> pathList = new LinkedList<Element>();

        while (e != null) {
            pathList.addFirst(e);
            e = e.getParentElement();
        }

        return pathList.toArray(new Element[pathList.size()]);
    }

    /**
     * Return the common parent of the two elements or null, if there is no common parent
     * 
     * @param e1 The first element
     * @param e2 The second element
     * @return the common ancestor
     */
    public Element getCommonParent(Element e1, Element e2) {
        if (e1 == null && e2 != null)
            return e2.getParentElement();
        else if (e1 != null && e2 == null)
            return e1.getParentElement();
        else if (e1 == null && e2 == null)
            return null;

        Element commonParent = null;

        Element[] ea1 = getPathArray(e1);
        Element[] ea2 = getPathArray(e2);
        for (int i = 0; i < (ea1.length - 1) && i < (ea2.length - 1); i++) {
            if (ea1[i] == ea2[i])
                commonParent = ea1[i];
            else
                break;
        }

        return commonParent;
    }

    /* Diese Methode pr�ft ob der �bergebene Tag sich in der Hierachie nach oben befindet */
    public boolean hasAncestor(int position, HTML.Tag tag) {
        Element e = context.getDocument().getParagraphElement(position);

        while (!isElementTag(e, HTML.Tag.HTML)) {
            if (isElementTag(e, tag))
                return true;

            e = e.getParentElement();
        }

        return false;
    }

    public HTMLEditorKit getKit() {
        return context.getKit();
    }

    public EditorDocument getDocument() {
        return context.getDocument();
    }

    public JEditorPane getPane() {
        return context.getPane();
    }

    public boolean isContainer(Element e) {
        return containers.contains(e.getName().toLowerCase());
    }

    public Element getContainer(Element e) {
        while (e != null) {
            if (isContainer(e))
                return e;

            e = e.getParentElement();
        }

        return null;
    }

    public boolean isStructureTag(String tag) {
        return structureParents.containsKey(tag);
    }

    public Element getStructureParent(Element e, String tag) {
        HTML.Tag parents[] = structureParents.get(tag);
        if (parents == null)
            return null;

        while (e != null) {
            for (HTML.Tag parent : parents)
                if (isElementTag(e, parent))
                    return e;

            e = e.getParentElement();
        }

        return null;
    }

    public Element getContainer(int position) {
        Element e = context.getDocument().getParagraphElement(position);
        return getContainer(e);
    }

    /* This method checks for an ancestor of with the given tag */
    public Element getAncestor(Element child, HTML.Tag tag) {
        Element e = child.getParentElement();

        while (e != null) {
            if (isElementTag(e, tag))
                return e;

            e = e.getParentElement();
        }

        return null;
    }

    /* This method checks for an ancestor of with the given name */
    public Element getAncestor(Element child, String name) {
        Element e = child.getParentElement();

        while (e != null) {
            if (e.getName().equalsIgnoreCase(name))
                return e;

            e = e.getParentElement();
        }

        return null;
    }

    /* This method checks for an ancestor of with the given tag */
    public Element getAncestor(int position, HTML.Tag tag) {
        Element e = context.getDocument().getParagraphElement(position);
        return getAncestor(e, tag);
    }

    public int getChildIndex(Element child) {
        if (child == null)
            return -1;
        Element parent = child.getParentElement();
        if (parent == null)
            return 0;

        for (int i = 0; i < parent.getElementCount(); i++) {
            if (parent.getElement(i) == child)
                return i;
        }

        return -1;
    }

    public int getChildIndex(Element parent, int pos) {
        for (int i = 0; i < parent.getElementCount(); i++) {
            Element e = parent.getElement(i);
            if (e.getStartOffset() <= pos && e.getEndOffset() > pos)
                return i;
        }

        return -1;
    }

    public Element getPreviousSibling(Element element) {
        Element parent = element.getParentElement();
        if (parent == null)
            return null;

        Element previous = null;
        for (int i = 0; i < parent.getElementCount(); i++) {
            if (parent.getElement(i) == element)
                return previous;
            previous = parent.getElement(i);
        }

        return null;
    }

    public Element getNextSibling(Element element) {
        Element parent = element.getParentElement();
        if (parent == null)
            return null;

        for (int i = 0; i < parent.getElementCount(); i++) {
            if (parent.getElement(i) == element)
                if (i + 1 < parent.getElementCount())
                    return parent.getElement(i + 1);
                else
                    return null;
        }

        return null;
    }

    /**
     * Find next element of the given tag type, starting from the element supplied.
     * 
     * @param e The element to start from
     * @param tag The tag
     * @return The element found
     */
    public Element findNextElement(Element e, String tag) {
        if (e.getName().equals(tag))
            return e;

        for (int i = 0; i < e.getElementCount(); i++) {
            Element found = findNextElement(e.getElement(i), tag);
            if (found != null)
                return found;
        }

        return null;
    }

    protected String firstTag(String html) {
        if (html.indexOf("<") < 0 || html.indexOf(">") < 0)
            return html;
        return html.substring(html.indexOf("<") + 1, html.indexOf(">"));
    }

    protected String lastTag(String html) {
        if (html.indexOf("<") < 0 || html.indexOf(">") < 0)
            return html;
        return html.substring(html.lastIndexOf("<") + 1, html.lastIndexOf(">"));
    }

    protected String removeLastTag(String html) {
        if (html.indexOf("<") < 0)
            return html;
        return html.substring(0, html.lastIndexOf("<"));
    }

    protected String removeFirstTag(String html) {
        if (html.indexOf(">") < 0)
            return html;
        return html.substring(html.indexOf(">") + 1);
    }

    public String getFirstTag(String html) {
        int start = html.indexOf("<");
        if (start < 0)
            return "";
        int end = html.indexOf(">");
        if (end < 0 || end < start)
            return "";

        String tagWithAttributes = html.substring(start + 1, end).trim().toLowerCase();
        int startSpace = tagWithAttributes.indexOf(" ");
        if (startSpace < 0)
            return tagWithAttributes;
        return tagWithAttributes.substring(0, startSpace);
    }

    protected String getInnerHTML(Element e, String html) {
        if (e.getParentElement() != null)
            html = getInnerHTML(e.getParentElement(), html);

        html = getInnerHTML(e.getName(), html);
        return html;
    }

    public String getOuterHTML(Element e, String innerHtml) {
        if (e.getName().equals(HTML.Tag.IMPLIED.toString()))
            return innerHtml;

        return getStartTag(e.getAttributes()) + innerHtml + getEndTag(e.getAttributes());
    }

    public static String getInnerHTML(String tag, String outerHtml) {
        if (tag.equals(HTML.Tag.IMG.toString()))
            return " ";
        
        if (outerHtml.indexOf("<" + tag) < 0)
            return outerHtml;

        int last = outerHtml.lastIndexOf("</" + tag);
        return outerHtml.substring(
                outerHtml.indexOf(">", outerHtml.indexOf("<" + tag)) + 1, last);
    }

    public String getInnerHTML(String outerHtml) {
        return getInnerHTML(getFirstTag(outerHtml), outerHtml);
    }

    class CondensedHTMLWriter extends HTMLWriter {
        int originalIndentSpace;
        String originalLineSeparator;

        public CondensedHTMLWriter(Writer w, HTMLDocument doc, int pos, int len) {
            super(w, doc, pos, len);
            this.originalLineSeparator = getLineSeparator();
            this.originalIndentSpace = getIndentSpace();
            setCondensed(true);
        }

        protected void startTag(Element elem) throws IOException, BadLocationException {
            if (elem.getName().equals(HTML.Tag.PRE.toString()))
                setCondensed(false);
            super.startTag(elem);
        }

        protected void endTag(Element elem) throws IOException {
            super.endTag(elem);
            if (elem.getName().equals(HTML.Tag.PRE.toString()))
                setCondensed(true);
        }

        public void setCondensed(boolean condensed) {
            if (condensed) {
                setLineSeparator("");
                setIndentSpace(0);
            } else {
                setLineSeparator(originalLineSeparator);
                setIndentSpace(originalIndentSpace);
            }
        }

    }

    public String getInnerHTML(Element e, int start, int end) throws BadLocationException, IOException {
        StringWriter writer = new StringWriter();
        HTMLWriter htmlWriter = null;
        if (e.getName().equals(HTML.Tag.PRE.toString()))
            htmlWriter = new HTMLWriter(writer, getDocument(), start, end - start);
        else 
            htmlWriter = new CondensedHTMLWriter(writer, getDocument(), start, end - start);
        
        htmlWriter.write();

        return getInnerHTML(e, writer.toString());
    }

    public String getOuterHTML(Element e) throws BadLocationException, IOException {
        return getOuterHTML(e, getInnerHTML(e));
    }

    public String getHTML(Element e) throws BadLocationException {
        if (e.getName().equals(HTML.Tag.IMPLIED.toString()) || e.getName().equals(HTML.Tag.CONTENT.toString()))
            return getContent(e);

        StringBuffer buf = new StringBuffer();

        buf.append(getStartTag(e));
        for (int i = 0; i < e.getElementCount(); i++) {
            buf.append(getHTML(e.getElement(i)));
        }
        buf.append(getEndTag(e));

        return buf.toString();
    }

    public String stripEnvelopeWhiteSpace(String in) {
        int start = 0;
        int end = 0;

        for (int i = 0; i < in.length(); i++) {
            if (!Character.isWhitespace(in.charAt(i))) {
                start = i;
                break;
            }
        }

        for (int i = in.length() - 1; i >= 0; i--) {
            if (!Character.isWhitespace(in.charAt(i))) {
                end = i;
                break;
            }
        }

        return in.substring(start, end + 1);
    }

    public String getInnerHTML(Element e) throws BadLocationException, IOException {
        return getInnerHTML(e, e.getStartOffset(), e.getEndOffset());
    }

    public void splitElement(Element e, int pos) throws BadLocationException, IOException {
        splitElementAndInsertBetween(e, pos, "");
        //getDocument().insertAfterEnd(e, second); workaround in jdk 6 no longer necessary, even causes loops.
    }

     public void splitElementAndInsertBetween(Element e, int pos, String html) throws BadLocationException, IOException {
        String first = getOuterHTML(e, getInnerHTML(e, e.getStartOffset(), pos));
        String second = getOuterHTML(e, getInnerHTML(e, pos, e.getEndOffset()));
        getDocument().setOuterHTML(e, first + html + second);
        //getDocument().insertAfterEnd(e, second); workaround in jdk 6 no longer necessary, even causes loops.
    }

    /**
     * Remove an element
     * 
     * @param e
     * @throws BadLocationException
     * @throws IOException
     */
    public void remove(Element e) throws BadLocationException, IOException {
        Element parent = e.getParentElement();

        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < parent.getElementCount(); i++) {
            if (parent.getElement(i) == e)
                continue;

            buf.append(getOuterHTML(parent.getElement(i)));
        }

        getDocument().setInnerHTML(parent, buf.toString());
    }

    /**
     * Remove multiple children of the same parent
     *
     * @param parent
     * @param children
     * @throws BadLocationException
     * @throws IOException
     */
    public void removeChildren(Element parent, HashSet<Element> children) throws BadLocationException, IOException {
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < parent.getElementCount(); i++) {
            if (children.contains(parent.getElement(i)))
                continue;

            buf.append(getOuterHTML(parent.getElement(i)));
        }

        getDocument().setInnerHTML(parent, buf.toString());
    }

    /*
     * Used in cases where getDocument().setOuterHTML causes problems. E.g. when substituting ul li/ li/ /ul by ul li/ /ul li/. 
     * In fact the call to setOuterHTML is replaced by a call to setInnerHTML of the parent element, which is easer for the parser and thus should 
     * cause less problems.
     */
    public void setOuterHTML(Element e, String html) throws BadLocationException, IOException {
        Element parent = e.getParentElement();
        if (parent == null)
            return;

        getDocument().setInnerHTML(parent, substituteElementByOuterHTMLGetParentInnerHTML(e, html));
    }

    public String getStartTag(AttributeSet atts) {
        return getStartTag(atts, null);
    }

    public String getStartTag(AttributeSet atts, String additionalAttribute) {
        StringBuffer buf = new StringBuffer();

        Object name = atts.getAttribute(StyleConstants.NameAttribute);
        if (name.equals(HTML.Tag.IMPLIED))
            return "";

        buf.append("<" + name);

        StringBuffer style = new StringBuffer();
        Enumeration i = atts.getAttributeNames();
        while (i.hasMoreElements()) {
            Object key = i.nextElement();
            if (key.equals(StyleConstants.NameAttribute))
                continue;
            Object value = atts.getAttribute(key);
            value = XmlUtils.attributeValueToXML(value.toString());
            if (key instanceof CSS.Attribute)
                style.append(" " + key + ":" + value + ";");
            else
                buf.append(" " + key + "=\"" + value + "\"");
        }

        if (style.length() > 0)
            buf.append(" style = \"" + style.toString() + "\"");

        if (additionalAttribute != null)
            buf.append(" " + additionalAttribute);

        buf.append(">");

        return buf.toString();
    }

    public String getStartTag(Element e) {
        return getStartTag(e.getAttributes());
    }

    public String getStartTag(Element e, String additionalAttribute) {
        return getStartTag(e.getAttributes(), additionalAttribute);
    }

    public String getStartTag(Element e, String key, String value) {
        return getStartTag(e, key + "=\"" + value + "\"");
    }

    public String getEndTag(AttributeSet atts) {
        Object name = atts.getAttribute(StyleConstants.NameAttribute);
        if (name.equals(HTML.Tag.IMPLIED))
            return "";
        return "</" + name + ">";
    }

    public String getEndTag(Element e) {
        return getEndTag(e.getAttributes());
    }

    public String getContent(Element e) throws BadLocationException {
        return getDocument().getText(e.getStartOffset(), e.getEndOffset() - e.getStartOffset());
    }

    public void addAttribute(Element e, Object name, Object value) {
        SimpleAttributeSet atts = new SimpleAttributeSet(e.getAttributes());
        atts.addAttribute(name, value);
        getDocument().setElementAttributes(e, atts, true);
    }

    public void removeAttribute(Element e, Object name) {
        SimpleAttributeSet atts = new SimpleAttributeSet(e.getAttributes());
        atts.removeAttribute(name);
        getDocument().setElementAttributes(e, atts, true);
    }
    
    public AttributeSet substituteAttribute(AttributeSet attributes, Object name, Object value) {
        SimpleAttributeSet atts = new SimpleAttributeSet(attributes);
        atts.removeAttribute(name);
        atts.addAttribute(name, value);
        
        return atts;
    }

    class InlineHtmlAttributeSet implements AttributeSet {
        Element parent;
        AttributeSet atts;

        public InlineHtmlAttributeSet(Element parent, AttributeSet atts) {
            this.parent = parent;
            this.atts = atts;
        }

        @Override
        public boolean containsAttribute(Object name, Object value) {
            if (atts.containsAttribute(name, value))
                return true;
            return parent.getAttributes().containsAttribute(name, value);
        }

        @Override
        public boolean containsAttributes(AttributeSet attributes) {
            if (atts.containsAttributes(attributes))
                return true;
            return parent.getAttributes().containsAttributes(attributes);
        }

        @Override
        public AttributeSet copyAttributes() {
            return new InlineHtmlAttributeSet(parent, atts);
        }

        @Override
        public Object getAttribute(Object key) {
            Object value = atts.getAttribute(key);
            if (value != null)
                return value;
            return parent.getAttributes().getAttribute(key);
        }

        @Override
        public int getAttributeCount() {
            return atts.getAttributeCount();
        }

        @Override
        public Enumeration<?> getAttributeNames() {
            return atts.getAttributeNames();
        }

        @Override
        public AttributeSet getResolveParent() {
            return parent.getAttributes();
        }

        @Override
        public boolean isDefined(Object attrName) {
            return atts.isDefined(attrName);
        }

        @Override
        public boolean isEqual(AttributeSet attr) {
            if (atts.getAttributeCount() != attr.getAttributeCount())
                return false;
            if (!parent.getAttributes().isEqual(attr.getResolveParent()))
                return false;
            Enumeration i = atts.getAttributeNames();
            while (i.hasMoreElements()) {
                Object name = i.nextElement();
                if (!atts.getAttribute(name).equals(attr.getAttribute(name)))
                    return false;
            }

            return true;
        }

    }

    /**
     * Artificial element for comparing Swing element structure with
     * HTML element structure. Substitutes the content of elements with
     * a single inline element content;
     */
    public class InlineHtmlElement implements Element {
        Element e;
        AttributeSet atts;

        public InlineHtmlElement(Element e, AttributeSet atts) {
            if (isArtificialElement(e)) // remove it, it does not match to the HTML elements
                e = e.getParentElement();
            this.e = e;
            this.atts = atts;
        }

        @Override
        public AttributeSet getAttributes() {
            return new InlineHtmlAttributeSet(e, atts);
        }

        @Override
        public Document getDocument() {
            return e.getDocument();
        }

        @Override
        public Element getElement(int index) {
            throw new IndexOutOfBoundsException();
        }

        @Override
        public int getElementCount() {
            return 0;
        }

        @Override
        public int getElementIndex(int offset) {
            throw new IndexOutOfBoundsException();
        }

        @Override
        public int getEndOffset() {
            return e.getEndOffset();
        }

        @Override
        public String getName() {
            HTML.Tag tag = (HTML.Tag) atts.getAttribute(StyleConstants.NameAttribute);
            return tag.toString();
        }

        @Override
        public Element getParentElement() {
            return e;
        }

        @Override
        public int getStartOffset() {
            return e.getStartOffset();
        }

        @Override
        public boolean isLeaf() {
            return true;
        }
    }

    /**
     * Artificial element for comparing Swing element structure with
     * HTML element structure. Substitutes elements with
     * a single inline element content;
     */
    class HtmlElement implements Element {
        Element e;

        public HtmlElement(Element e) {
            this.e = e;
        }

        @Override
        public AttributeSet getAttributes() {
            return e.getAttributes();
        }

        @Override
        public Document getDocument() {
            return e.getDocument();
        }

        @Override
        public Element getElement(int index) {
            if (index > 1)
                throw new IndexOutOfBoundsException();
            SimpleAttributeSet atts = new SimpleAttributeSet(getInlineAttributes(e.getElement(0)));
            HTML.Tag tag = getInlineElementTag(e.getElement(0).getAttributes());
            atts.addAttribute(StyleConstants.NameAttribute, tag);

            return new InlineHtmlElement(e, atts);
        }

        @Override
        public int getElementCount() {
            return 1;
        }

        @Override
        public int getElementIndex(int offset) {
            return 0;
        }

        @Override
        public int getEndOffset() {
            return e.getEndOffset();
        }

        @Override
        public String getName() {
            return e.getName();
        }

        @Override
        public Element getParentElement() {
            return e.getParentElement();
        }

        @Override
        public int getStartOffset() {
            return e.getStartOffset();
        }

        @Override
        public boolean isLeaf() {
            return e.isLeaf();
        }

    }

    /**
     * Does the element have content, which represents an
     * inline element tag and does it have only a single
     * such inline element?
     * 
     * @param e
     * @return
     */
    public boolean hasSubstitutableInlineElement(Element e) {
        if (hasInlineElement(e) && e.getElementCount() == 2) // single span
            return true;
        return false;
    }

    /**
     * Used to convert a SWING element into an artificial element
     * more similar to the HTML element structure.
     * 
     * @param e
     * @return
     */
    public Element getHtmlElement(Element e) {
        if (hasSubstitutableInlineElement(e))
            return new HtmlElement(e);
        if (isArtificialElement(e))
            return e.getParentElement();
        return e;
    }

    /**
     * Some elements like TD cannot directly have content, but require some
     * intermediate artificial pseudo paragraph element IMPLIED.
     *
     * @param e
     * @return Is this such an element?
     */
    public boolean isArtificialElement(Element e) {
        if (e.getName().equals(HTML.Tag.IMPLIED.toString()))
            return true;
        return false;
    }

    /**
     * The content element represents an inline element, if one of its attributes is an HTML tag.
     * 
     * @param e The potential inline element
     * @return The element tag, if there is any. Else null.
     */
    public HTML.Tag getInlineElementTag(Element e) {
        return getInlineElementTag(e.getAttributes());
    }

    /**
     * The content element represents an inline element, if one of its attributes is an HTML tag.
     * 
     * @param atts The attribute set of the potential inline element
     * @return The element tag, if there is any. Else null.
     */
    public HTML.Tag getInlineElementTag(AttributeSet atts) {
        Enumeration en = atts.getAttributeNames();
        while (en.hasMoreElements()) {
            Object next = en.nextElement();
            if (next instanceof HTML.Tag)
                return ((HTML.Tag) next);
        }
        return null;
    }

    /**
     * Does the element represent an inline element?
     * 
     * The swing text element model does not model inline elements (SPAN, A, ...) as separate elements. Rather, they are
     * modeled below its container branch element as a set of content elements. 
     * The content element representing the inline element has a special attribute: The HTML tag as key and an attribute set with the
     * inline element's attributes.
     * 
     * Note, that this kind of representation puts a heavy burden on dealing with the inline elements. All scattered in your generic code, you will 
     * have conditions specific for the inline elements, which are difficult to maintain. We could deal with that by a specific element layer used 
     * in substitution for the swing elements, which would make up virtual inline elements, but that would mean a major refactoring.
     * 
     * @param e The potential inline element
     * @return true, if this is an inline element. Else false.
     */
    public boolean isInlineElement(Element e) {
        if (!e.getName().equals(HTML.Tag.CONTENT.toString()))
            return false;
        return getInlineElementTag(e) != null;
    }

    /**
     * Return the AttributeSet associated to the inline element
     * 
     * @param e The inline element
     * @return The inline attributes, or null, if this is no inline element at all
     */
    public AttributeSet getInlineAttributes(Element e) {
        HTML.Tag tag = getInlineElementTag(e);
        if (tag != null)
            return (AttributeSet) e.getAttributes().getAttribute(tag);

        return null;
    }
    
    /**
     * Create an element from an inline element,
     * that behaves more like usual elements.
     * 
     * @param e the inline element
     * @return the more usual element
     */
    public Element getInlineHTMLELement(Element e) {
        if (!isInlineElement(e)) {
            return null;
        }

        SimpleAttributeSet atts = new SimpleAttributeSet(getInlineAttributes(e));
        HTML.Tag tag = getInlineElementTag(e.getAttributes());
        atts.addAttribute(StyleConstants.NameAttribute, tag);

        return new InlineHtmlElement(e, atts);
    }

    /**
     * Is this an artificial p-implied paragraph (used e.g. for td etc.) with an inline content element?
     *
     * @param e
     * @return
     */
    public boolean isArtificialWithInlineElement(Element e) {
        String name = e.getName();
        if (name.equals(HTML.Tag.IMPLIED.toString()))
            for (int i = 0; i < e.getElementCount(); i++) {
                if (isInlineElement(e.getElement(i)))
                    return true;
            }

        return false;
    }

    /**
     * Is this an artificial p-implied paragraph (used e.g. for td etc.) with an inline content element?
     *
     * @param e
     * @return
     */
    public boolean hasInlineElement(Element e) {
        for (int i = 0; i < e.getElementCount(); i++) {
            if (isInlineElement(e.getElement(i)))
                return true;
        }

        return false;
    }

    /**
     * The element structure of an HTML element like "abc<span a='d'>def</span>"
     * consists of three leaf content elements in the container element, e.g. a paragraph. The
     * second leaf content element has a special attribute of HTML.Tag.SPAN. The value of this attribute
     * is an AttributeSet again, now with the attributes of the inline element span, here a='d'.
     * 
     * This method creates in reverse the HTML as in the example above from that swing element structure and
     * returns it.
     * 
     * @param e the container with the inline element, e.g. a p-implied.
     * @return the HTML string
     */
    public String getInlineHTML(Element e) throws Exception {
        if (!isArtificialWithInlineElement(e))
            return "";

        StringBuffer buf = new StringBuffer();
        for (int i = 0; i < e.getElementCount(); i++) {
            HTML.Tag tag = getInlineElementTag(e.getElement(i));
            if (tag != null) {
                SimpleAttributeSet atts = new SimpleAttributeSet((AttributeSet) e.getElement(i).getAttributes().getAttribute(tag));
                atts.addAttribute(StyleConstants.NameAttribute, tag);

                buf.append(getStartTag(atts));
                buf.append(getContent(e.getElement(i)));
                buf.append(getEndTag(atts));
            } else
                buf.append(getContent(e.getElement(i)));
        }

        return buf.toString();
    }

    /**
     *
     * @param e
     * @return
     */
    public boolean isLeaf(Element e) {
        if (e.isLeaf())
            return true;

        for (int i = 0; i < e.getElementCount(); i++)
            if (!e.getElement(i).getName().equals(HTML.Tag.CONTENT.toString()))
                return false;

        return true;
    }

    /**
     * Return the first paragraph element enclosing the given element e, that has no p-implied children.
     * It is necessary to find that as a part of a workaround of a swing bug.
     * 
     * @param e the element, for which the paragraph element is searched
     * @return the enclosing paragraph element adhering to the conditions described above
     */
    public Element getParagraphWithNoImpliedSiblings(Element e) {
        if (e.getName().equals(HTML.Tag.IMPLIED.toString()))
            return getParagraphWithNoImpliedSiblings(e.getParentElement());

        boolean hasImpliedSibling = false;
        Element parent = e.getParentElement();
        for (int i = 0; i < parent.getElementCount(); i++) {
            if (parent.getElement(i).getName().equals(HTML.Tag.IMPLIED.toString())) {
                hasImpliedSibling = true;
                break;
            }
        }

        if (hasImpliedSibling)
            return getParagraphWithNoImpliedSiblings(e.getParentElement());

        return e;
    }

    /**
     * Return the html of a leaf element, thereby substituting its content. 
     * 
     * If the leaf element corresponds to an inline element, this will include the start and end tags
     * of the inline element, else only the content.
     * 
     * @param content The new content
     * @param e the leaf element
     * @return the new html
     * @throws javax.swing.text.BadLocationException
     */
    protected String getLeafHtml(String content, Element e) throws BadLocationException {
        StringBuffer buf = new StringBuffer();

        HTML.Tag tag = getInlineElementTag(e);
        if (tag != null) {
            SimpleAttributeSet atts = new SimpleAttributeSet((AttributeSet) e.getAttributes().getAttribute(tag));
            atts.addAttribute(StyleConstants.NameAttribute, tag);

            buf.append(getStartTag(atts));
            buf.append(content);
            buf.append(getEndTag(atts));
        } else
            buf.append(content);

        return buf.toString();
    }

    /**
     * Substitute a span defined by range within the element e by a given html string.
     *
     * @param e the element, that contains the span
     * @param html the html string
     * @param range the range, that describes the borders of the span
     * @return the html string corresponding to e
     *
     * @throws javax.swing.text.BadLocationException
     * @throws java.io.IOException
     * @throws java.lang.Exception
     */
    protected String substituteRange(Element e, String html, int startOffset, int endOffset) throws BadLocationException, IOException, Exception {
        StringBuilder buf = new StringBuilder();

        buf.append(getStartTag(e));

        buf.append(getInnerHTML(e, e.getStartOffset(), startOffset));
        buf.append(html);
        buf.append(getInnerHTML(e, endOffset, e.getEndOffset()));
        
        buf.append(getEndTag(e));

        return buf.toString();
    }

    /**
     * Create the inner HTML of the parent of the element, substituting the element
     * itself by the html passed.
     * 
     * @param e the element
     * @param html the HTML to substitute the element within the parent
     * @return the inner HTML of the parent
     * @throws BadLocationException
     * @throws IOException 
     */
   protected String substituteElementByOuterHTMLGetParentInnerHTML(Element e, String html) throws BadLocationException, IOException {
        Element parent = e.getParentElement();
        if (parent == null)
            return "";

        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < parent.getElementCount(); i++) {
            Element child = parent.getElement(i);
            if (child == e)
                buf.append(html);
            else
                buf.append(getOuterHTML(child));
        }

        return buf.toString();

   }

   public Rectangle getVisibleEditorRect() {
	Rectangle alloc = context.getPane().getBounds();
	if ((alloc.width > 0) && (alloc.height > 0)) {
	    alloc.x = alloc.y = 0;
	    Insets insets = context.getPane().getInsets();
	    alloc.x += insets.left;
	    alloc.y += insets.top;
	    alloc.width -= insets.left + insets.right;
	    alloc.height -= insets.top + insets.bottom;
	    return alloc;
	}
	return null;
    }

    public int getViewIndex(View parent, Element childElement) {
        for (int i = 0; i < parent.getViewCount(); i++) {
            if (parent.getView(i).getElement() == childElement)
                return i;
        }

        return -1;
    }

    public Shape getElementShape(Element e) {
        Element[] path = getPathArray(e);
        View v = context.getPane().getUI().getRootView(context.getPane());
        Shape shape = getVisibleEditorRect();

        for (int i = 0; i < path.length; i++) {
            int child = getViewIndex(v, path[i]);
            shape = v.getChildAllocation(child, shape);
            v = v.getView(child);
        }

        return shape;
    }
}
